using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.Serialization;

using Pegasus.Diagnostics;
using Pegasus.Runtime.Serialization;
using Pegasus.Reflection;

namespace Pegasus.Workflow.Service
{
	/// <summary>
	/// This is the persistence context for a workflow.  Only data withing this object is 
	/// persistected 
	/// </summary>
	[Serializable]
	public class WorkflowContext : IDisposable, ISerializable, IDictionary< string, object >
	{
		// Local Instance Values
		private WorkflowService m_workflowService;
		private bool m_readOnly = false;

		private Type m_workflowType;
		private Workflow m_workflow;
		private Dictionary<string, object> m_objectTable = new Dictionary<string, object>();
		private Dictionary<Type, object> m_localServiceTable = new Dictionary<Type, object>();

		// Local Const Values
		private const string ContextWorkflowType = "WorkflowContext.WorkflowType";

		/// <summary>
		/// Initializes a new instance of the <see cref="T:WorkflowContext"/> class.  This class
		/// can only be created withing the workflow service.
		/// </summary>
		/// <param name="workflowService">The workflow service.</param>
		/// <param name="workflowType">Type of the workflow.</param>
		internal WorkflowContext( WorkflowService workflowService, Type workflowType  )
		{
			// Check Parameters
			ParamCode.AssertNotNull( workflowService, "workflowService" );
			ParamCode.AssertNotNull( workflowType, "workflowType" );
			ParamCode.Assert( workflowType.IsSubclassOf( typeof( Workflow ) ), "workflowType", "Workflow type {0} is not subclass of workflow", workflowType );

			m_workflowService = workflowService;
			m_workflowType = workflowType;
			m_workflow = (Workflow) WorkflowService.CreateObjectFromType( m_workflowType );
		}

		/// <summary>
		/// Initializes a new instance of the <see cref="T:WorkflowContext"/> class from a persistence object.
		/// </summary>
		/// <param name="info">The info.</param>
		/// <param name="context">The context.</param>
		protected WorkflowContext( SerializationInfo info, StreamingContext context )
		{
			// Check Parameters
			ParamCode.AssertNotNull( info, "info" );
			ParamCode.AssertNotNull( context, "context" );

			foreach( SerializationEntry entry in info )
			{
				switch( entry.Name )
				{
					case ContextWorkflowType:
                        ObjectSerializationBinder binder = new ObjectSerializationBinder();
                        m_workflowType = binder.BindToType( new TypeName( (string) entry.Value ) );
						break;

					default:
						Add( entry.Name, entry.Value );
						break;
				}
			}

			// Make sure we have a valid workflow type object
			ParamCode.AssertNotNull( m_workflowType, "m_workflowType" );
			ParamCode.Assert( m_workflowType.IsSubclassOf( typeof( Workflow ) ), "m_workflowType", "Workflow type {0} is not subclass of workflow", m_workflowType );

			m_workflow = (Workflow) WorkflowService.CreateObjectFromType( m_workflowType );
		}

		/// <summary>
		/// Gets the workflow service.
		/// </summary>
		/// <value>The workflow service.</value>
		public WorkflowService WorkflowService
		{
			get
			{
				return m_workflowService;
			}
		}

		/// <summary>
		/// Gets the workflow id.
		/// </summary>
		/// <value>The workflow id.</value>
		public int WorkflowId
		{
			get
			{
				return m_workflow.WorkflowId;
			}
		}

		/// <summary>
		/// Gets the workflow for this context.
		/// </summary>
		/// <value>The workflow.</value>
		public Workflow Workflow
		{
			get
			{
				return m_workflow;
			}
		}

		/// <summary>
		/// Gets the number of elements contained in the <see cref="T:System.Collections.Generic.ICollection`1"></see>.
		/// </summary>
		/// <value></value>
		/// <returns>The number of elements contained in the <see cref="T:System.Collections.Generic.ICollection`1"></see>.</returns>
		public int Count
		{
			get
			{
				return m_objectTable.Count;
			}
		}

		/// <summary>
		/// Gets a value indicating whether the <see cref="T:System.Collections.Generic.ICollection`1"></see> is read-only.
		/// </summary>
		/// <value></value>
		/// <returns>true if the <see cref="T:System.Collections.Generic.ICollection`1"></see> is read-only; otherwise, false.</returns>
		public bool IsReadOnly
		{
			get
			{
				return m_readOnly;
			}
		}

		/// <summary>
		/// Gets a value indicating whether this instance is locked.
		/// </summary>
		/// <value><c>true</c> if this instance is locked; otherwise, <c>false</c>.</value>
		public bool IsLocked
		{
			get
			{
				// If the context is in memory and is read/write then it is locked.
				return !m_readOnly;
			}
		}

		/// <summary>
		/// Gets an <see cref="T:System.Collections.Generic.ICollection`1"></see> containing the keys of the <see cref="T:System.Collections.Generic.IDictionary`2"></see>.
		/// </summary>
		/// <value></value>
		/// <returns>An <see cref="T:System.Collections.Generic.ICollection`1"></see> containing the keys of the object that implements <see cref="T:System.Collections.Generic.IDictionary`2"></see>.</returns>
		public ICollection<string> Keys
		{
			get
			{
				return m_objectTable.Keys;
			}
		}

		/// <summary>
		/// Gets an <see cref="T:System.Collections.Generic.ICollection`1"></see> containing the values in the <see cref="T:System.Collections.Generic.IDictionary`2"></see>.
		/// </summary>
		/// <value></value>
		/// <returns>An <see cref="T:System.Collections.Generic.ICollection`1"></see> containing the values in the object that implements <see cref="T:System.Collections.Generic.IDictionary`2"></see>.</returns>
		public ICollection<object> Values
		{
			get
			{
				return m_objectTable.Values;
			}
		}

		/// <summary>
		/// Gets or sets the <see cref="T:Object"/> with the specified key.
		/// </summary>
		/// <value></value>
		public object this[ string key ]
		{
			get
			{
				// Check Parameters 
				ParamCode.AssertNotEmpty( key, "key" );

				if( m_objectTable.ContainsKey( key ) )
				{
					return m_objectTable[ key ];
				}

				return null;
			}

			set
			{
				// Check Parameters 
				ParamCode.AssertNotEmpty( key, "key" );
				// value can be null

				ThrowIfReadOnly();
				Add( key, value );
			}
		}

		/// <summary>
		/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
		/// </summary>
		public void Dispose()
		{
			if( !m_readOnly )
			{
				// Save the workflow and unlock it.
				Save( true );
			}
		}

		/// <summary>
		/// Saves this instance.
		/// </summary>
		public void Save()
		{
			Save( false );
		}

		/// <summary>
		/// Saves this instance.
		/// </summary>
		/// <param name="unlock">if set to <c>true</c> saves and then unlocks the context.</param>
		public void Save( bool unlock )
		{
			if( !m_readOnly )
			{
				m_workflowService.GetService<WorkflowPersistenceService>().SaveWorkflowContext( this, unlock );
			}
		}

		/// <summary>
		/// Populates a <see cref="T:System.Runtime.Serialization.SerializationInfo"></see> with the data needed to serialize the target object.
		/// </summary>
		/// <param name="info">The <see cref="T:System.Runtime.Serialization.SerializationInfo"></see> to populate with data.</param>
		/// <param name="context">The destination (see <see cref="T:System.Runtime.Serialization.StreamingContext"></see>) for this serialization.</param>
		/// <exception cref="T:System.Security.SecurityException">The caller does not have the required permission. </exception>
		public void GetObjectData( SerializationInfo info, StreamingContext context )
		{
			// Check Parameters
			ParamCode.AssertNotNull( info, "info" );
			ParamCode.AssertNotNull( context, "context" );

			// Add the local context values
			info.AddValue( ContextWorkflowType, m_workflowType.AssemblyQualifiedName );

			// Save the object table.  Normally we would just call the GetObjectData() method but the 
			// Dictionary<> object will throw if it contains any objects that are not marked Serializable.  To get
			// around this we just save the items ourself.
			foreach( KeyValuePair<string, object> item in this )
			{
				// Check to see if we should not save the object
				bool save = true;

				// If the key start with an underscore then don't save it.
				if( item.Key[ 0 ] == '_' )
				{
					save = false;
				}
				// If the we have a value that is not serializable then don't save it.
				else if( item.Value != null && !item.Value.GetType().IsSerializable )
				{
					save = false;
				}

				// Add the object if we can
				if( save )
				{
					info.AddValue( item.Key, item.Value );
				}
			}
		}

		/// <summary>
		/// Gets the service.
		/// </summary>
		/// <returns></returns>
		public T GetService<T>()
		{
			return (T) GetService( typeof( T ) );
		}

		/// <summary>
		/// Gets the service object of the specified type.
		/// </summary>
		/// <param name="serviceType">An object that specifies the type of service object to get.</param>
		/// <returns>
		/// A service object of type serviceType.-or- null if there is no service object of type serviceType.
		/// </returns>
		public object GetService( Type serviceType )
		{
			// Check Parameters
			ParamCode.AssertNotNull( serviceType, "serviceType" );

			// Do we have a copy of the service in our local service table.
			if( m_localServiceTable.ContainsKey( serviceType ) )
			{
				return m_localServiceTable[ serviceType ];
			}

			// Goto the workflow service and see if it has it one
			object service = m_workflowService.GetService( serviceType );
			if( service != null )
			{
				// If this service is a data exchange service then create a stub version
				// that the workflow can bind to.
				if( serviceType.IsSubclassOf( typeof( WorkflowDataExchangeService ) ) )
				{
					service = WorkflowService.CreateObjectFromType( serviceType );
					if( service == null )
					{
						throw new WorkflowException( "Could not create an instance of the workflow data exchange service {0}, ensure that it has a default constructor.", serviceType );
					}

					m_localServiceTable.Add( serviceType, service );
				}
			}

			return service;
		}

		/// <summary>
		/// Adds an element with the provided key and value to the <see cref="T:System.Collections.Generic.IDictionary`2"></see>.
		/// </summary>
		/// <param name="key">The object to use as the key of the element to add.</param>
		/// <param name="value">The object to use as the value of the element to add.</param>
		/// <exception cref="T:System.NotSupportedException">The <see cref="T:System.Collections.Generic.IDictionary`2"></see> is read-only.</exception>
		/// <exception cref="T:System.ArgumentException">An element with the same key already exists in the <see cref="T:System.Collections.Generic.IDictionary`2"></see>.</exception>
		/// <exception cref="T:System.ArgumentNullException">key is null.</exception>
		public void Add( string key, object value )
		{
			// Check Parameters
			ParamCode.AssertNotEmpty( key, "key" );
			// value can be null

			ThrowIfReadOnly();

			// If the key start with an '_' underscore then don't check if the
			// value is serialize because we wont try to persiste it.  If it's 
			// just a normal key then make sure that the value is a serializable
			// type.

			if( key[ 0 ] != '_' && value != null )
			{
				Type valueType = value.GetType();
				if( !valueType.IsSerializable )
				{
					throw new WorkflowException( "Can not add key {0} and value {1} because the type {2} is not serializable", key, value, valueType );
				}
			}

			m_objectTable[ key ] = value;
		}

		/// <summary>
		/// Determines whether the <see cref="T:System.Collections.Generic.IDictionary`2"></see> contains an element with the specified key.
		/// </summary>
		/// <param name="key">The key to locate in the <see cref="T:System.Collections.Generic.IDictionary`2"></see>.</param>
		/// <returns>
		/// true if the <see cref="T:System.Collections.Generic.IDictionary`2"></see> contains an element with the key; otherwise, false.
		/// </returns>
		/// <exception cref="T:System.ArgumentNullException">key is null.</exception>
		public bool ContainsKey( string key )
		{
			// Check Parameters
			ParamCode.AssertNotEmpty( key, "key" );

			return m_objectTable.ContainsKey( key );
		}

		/// <summary>
		/// Removes the element with the specified key from the <see cref="T:System.Collections.Generic.IDictionary`2"></see>.
		/// </summary>
		/// <param name="key">The key of the element to remove.</param>
		/// <returns>
		/// true if the element is successfully removed; otherwise, false.  This method also returns false if key was not found in the original <see cref="T:System.Collections.Generic.IDictionary`2"></see>.
		/// </returns>
		/// <exception cref="T:System.NotSupportedException">The <see cref="T:System.Collections.Generic.IDictionary`2"></see> is read-only.</exception>
		/// <exception cref="T:System.ArgumentNullException">key is null.</exception>
		public bool Remove( string key )
		{
			// Check Parameters
			ParamCode.AssertNotEmpty( key, "key" );

			ThrowIfReadOnly();
			return m_objectTable.Remove( key );
		}

		/// <summary>
		/// Tries the get value.
		/// </summary>
		/// <param name="key">The key.</param>
		/// <param name="value">The value.</param>
		/// <returns></returns>
		public bool TryGetValue( string key, out object value )
		{
			// Check Parameters
			ParamCode.AssertNotEmpty( key, "key" );
			// value can be null

			return m_objectTable.TryGetValue( key, out value );
		}

		/// <summary>
		/// Tries the get value of the given type.
		/// </summary>
		/// <param name="key">The key.</param>
		/// <param name="value">The value.</param>
		/// <returns></returns>
		public bool TryGetValue<T>( string key, out T value )
		{
			if( m_objectTable.ContainsKey( key ) )
			{
				object foundObject = m_objectTable[ key ];

				if( foundObject == null )
				{
					if( typeof( T ).IsValueType )
					{
						value = default( T );
						return false;
					}
				}

				try
				{
					value = (T) foundObject;
					return true;
				}
				catch( InvalidCastException )
				{
				}
			}

			value = default( T );
			return false;
		}

		/// <summary>
		/// Adds an item to the <see cref="T:System.Collections.Generic.ICollection`1"></see>.
		/// </summary>
		/// <param name="item">The object to add to the <see cref="T:System.Collections.Generic.ICollection`1"></see>.</param>
		/// <exception cref="T:System.NotSupportedException">The <see cref="T:System.Collections.Generic.ICollection`1"></see> is read-only.</exception>
		public void Add( KeyValuePair<string, object> item )
		{
			// Check Parameters
			ParamCode.AssertNotNull( item, "item" );
			ParamCode.AssertNotEmpty( item.Key, "item.Key" );
			// item.Value can be null

			ThrowIfReadOnly();
			Add( item.Key, item.Value );
		}

		/// <summary>
		/// Removes all items from the <see cref="T:System.Collections.Generic.ICollection`1"></see>.
		/// </summary>
		/// <exception cref="T:System.NotSupportedException">The <see cref="T:System.Collections.Generic.ICollection`1"></see> is read-only. </exception>
		public void Clear()
		{
			ThrowIfReadOnly();
			m_objectTable.Clear();
		}

		/// <summary>
		/// Determines whether the <see cref="T:System.Collections.Generic.ICollection`1"></see> contains a specific value.
		/// </summary>
		/// <param name="item">The object to locate in the <see cref="T:System.Collections.Generic.ICollection`1"></see>.</param>
		/// <returns>
		/// true if item is found in the <see cref="T:System.Collections.Generic.ICollection`1"></see>; otherwise, false.
		/// </returns>
		public bool Contains( KeyValuePair<string, object> item )
		{
			// Check Parameters
			ParamCode.AssertNotNull( item, "item" );
			ParamCode.AssertNotEmpty( item.Key, "item.Key" );
			// item.Value can be null

			if( m_objectTable.ContainsKey( item.Key ) )
			{
				object obj = m_objectTable[ item.Key ];
				if( obj != null )
				{
					return obj.Equals( item.Value );
				}
				else if( item.Value == null )
				{
					return true;
				}
			}

			return false;
		}

		/// <summary>
		/// Copies the elements of the <see cref="T:System.Collections.Generic.ICollection`1"></see> to an <see cref="T:System.Array"></see>, starting at a particular <see cref="T:System.Array"></see> index.
		/// </summary>
		/// <param name="array">The one-dimensional <see cref="T:System.Array"></see> that is the destination of the elements copied from <see cref="T:System.Collections.Generic.ICollection`1"></see>. The <see cref="T:System.Array"></see> must have zero-based indexing.</param>
		/// <param name="arrayIndex">The zero-based index in array at which copying begins.</param>
		/// <exception cref="T:System.ArgumentOutOfRangeException">arrayIndex is less than 0.</exception>
		/// <exception cref="T:System.ArgumentNullException">array is null.</exception>
		/// <exception cref="T:System.ArgumentException">array is multidimensional.-or-arrayIndex is equal to or greater than the length of array.-or-The number of elements in the source <see cref="T:System.Collections.Generic.ICollection`1"></see> is greater than the available space from arrayIndex to the end of the destination array.-or-Type T cannot be cast automatically to the type of the destination array.</exception>
		public void CopyTo( KeyValuePair<string, object>[] array, int arrayIndex )
		{
			int length = array.Length;
			int x = arrayIndex;
			
			foreach( KeyValuePair<string, object> item in m_objectTable )
			{
				array[ x ] = item;
				x++;
				if( x >= length )
				{
					// If the array if full then braek out of the for loop.
					break;
				}
			}
		}

		/// <summary>
		/// Removes the first occurrence of a specific object from the <see cref="T:System.Collections.Generic.ICollection`1"></see>.
		/// </summary>
		/// <param name="item">The object to remove from the <see cref="T:System.Collections.Generic.ICollection`1"></see>.</param>
		/// <returns>
		/// true if item was successfully removed from the <see cref="T:System.Collections.Generic.ICollection`1"></see>; otherwise, false. This method also returns false if item is not found in the original <see cref="T:System.Collections.Generic.ICollection`1"></see>.
		/// </returns>
		/// <exception cref="T:System.NotSupportedException">The <see cref="T:System.Collections.Generic.ICollection`1"></see> is read-only.</exception>
		public bool Remove( KeyValuePair<string, object> item )
		{
			// Check Parameters
			ParamCode.AssertNotNull( item, "item" );
			ParamCode.AssertNotEmpty( item.Key, "item.Key" );
			// item.Value can be null

			ThrowIfReadOnly();

			if( Contains( item ) )
			{
				return m_objectTable.Remove( item.Key );
			}

			return false;
		}

		/// <summary>
		/// Returns an enumerator that iterates through the collection.
		/// </summary>
		/// <returns>
		/// A <see cref="T:System.Collections.Generic.IEnumerator`1"></see> that can be used to iterate through the collection.
		/// </returns>
		public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
		{
			return m_objectTable.GetEnumerator();
		}

		/// <summary>
		/// Returns an enumerator that iterates through a collection.
		/// </summary>
		/// <returns>
		/// An <see cref="T:System.Collections.IEnumerator"></see> object that can be used to iterate through the collection.
		/// </returns>
		IEnumerator IEnumerable.GetEnumerator()
		{
			return m_objectTable.GetEnumerator();
		}

		/// <summary>
		/// Called by the persistence service to Initializes a new context object.
		/// </summary>
		/// <param name="workflowId">The workflow id.</param>
		internal void InitializeNew( int workflowId )
		{
			m_workflow.Initialize( this, workflowId );

			// Fire the initialize complete event.
			m_workflow.FireInitializeComplete();
		}

		/// <summary>
		/// Called by the persistence service to Initializes an existing context object.
		/// </summary>
		internal void InitializeExisting( WorkflowService workflowService, int workflowId, bool readOnly )
		{
			m_workflowService = workflowService;

			m_workflow.Initialize( this, workflowId );
			m_readOnly = readOnly;

			// Fire the initialize complete event.
			m_workflow.FireInitializeComplete();
		}

		/// <summary>
		/// Sets this context as read only.
		/// </summary>
		internal void SetAsReadOnly()
		{
			m_readOnly = true;
		}

		/// <summary>
		/// Throws if read only.
		/// </summary>
		private void ThrowIfReadOnly()
		{
			if( m_readOnly )
			{
				throw new WorkflowException( "Context object is read only" );
			}
		}
	}
}
